/*
 * Copyright 2004-2010, Thorbjrn Lindeijer <thorbjorn@lindeijer.nl>
 * Copyright 2004-2006, Adam Turk <aturk@biggeruniverse.com>
 *
 * This file is part of libtiled-java.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *
 *    2. Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 * EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package tiled.core;

import java.awt.Rectangle;
import java.util.Iterator;
import java.util.Properties;
import java.util.Vector;

/**
 * The Map class is the focal point of the <code>tiled.core</code> package.
 */
public class Map implements Iterable<MapLayer> {
	public static final int		ORIENTATION_ORTHOGONAL	= 1;
	public static final int		ORIENTATION_ISOMETRIC	= 2;
	public static final int		ORIENTATION_HEXAGONAL	= 4;
	/** Shifted (used for iso and hex). */
	public static final int		ORIENTATION_SHIFTED		= 5;

	private Vector<MapLayer>	layers;
	private Vector<TileSet>		tileSets;

	private int					tileWidth, tileHeight;
	private int					orientation				= ORIENTATION_ORTHOGONAL;
	private Properties			properties;
	private String				filename;
	protected Rectangle			bounds;											// in tiles

	/**
	 * @param width
	 *            the map width in tiles.
	 * @param height
	 *            the map height in tiles.
	 */
	public Map(int width, int height) {
		this.layers = new Vector<MapLayer>();
		this.bounds = new Rectangle(width, height);
		this.properties = new Properties();
		this.tileSets = new Vector<TileSet>();
	}

	/**
	 * Returns the total number of layers.
	 * 
	 * @return the size of the layer vector
	 */
	public int getLayerCount() {
		return this.layers.size();
	}

	/**
	 * Changes the bounds of this plane to include all layers completely.
	 */
	public void fitBoundsToLayers() {
		int width = 0;
		int height = 0;

		Rectangle layerBounds = new Rectangle();

		for (int i = 0; i < this.layers.size(); i++) {
			this.getLayer(i).getBounds(layerBounds);
			if (width < layerBounds.width) {
				width = layerBounds.width;
			}
			if (height < layerBounds.height) {
				height = layerBounds.height;
			}
		}

		this.bounds.width = width;
		this.bounds.height = height;
	}

	/**
	 * Returns a <code>Rectangle</code> representing the maximum bounds in tiles.
	 * 
	 * @return a new rectangle containing the maximum bounds of this plane
	 */
	public Rectangle getBounds() {
		return new Rectangle(this.bounds);
	}

	public MapLayer addLayer(MapLayer layer) {
		layer.setMap(this);
		this.layers.add(layer);
		return layer;
	}

	public void setLayer(int index, MapLayer layer) {
		layer.setMap(this);
		this.layers.set(index, layer);
	}

	public void insertLayer(int index, MapLayer layer) {
		layer.setMap(this);
		this.layers.add(index, layer);
	}

	/**
	 * Removes the layer at the specified index. Layers above this layer will move down to fill the gap.
	 * 
	 * @param index
	 *            the index of the layer to be removed
	 * @return the layer that was removed from the list
	 */
	public MapLayer removeLayer(int index) {
		return this.layers.remove(index);
	}

	/**
	 * Removes all layers from the plane.
	 */
	public void removeAllLayers() {
		this.layers.removeAllElements();
	}

	/**
	 * Returns the layer vector.
	 * 
	 * @return Vector the layer vector
	 */
	public Vector<MapLayer> getLayers() {
		return this.layers;
	}

	/**
	 * Sets the layer vector to the given java.util.Vector.
	 * 
	 * @param layers
	 *            the new set of layers
	 */
	public void setLayers(Vector<MapLayer> layers) {
		this.layers = layers;
	}

	/**
	 * Returns the layer at the specified vector index.
	 * 
	 * @param i
	 *            the index of the layer to return
	 * @return the layer at the specified index, or null if the index is out of bounds
	 */
	public MapLayer getLayer(int i) {
		try {
			return this.layers.get(i);
		} catch (ArrayIndexOutOfBoundsException e) {
		}
		return null;
	}

	/**
	 * Resizes this plane. The (dx, dy) pair determines where the original plane should be positioned on the new area.
	 * Only layers that exactly match the bounds of the map are resized, any other layers are moved by the given shift.
	 * 
	 * @see tiled.core.MapLayer#resize
	 * 
	 * @param width
	 *            The new width of the map.
	 * @param height
	 *            The new height of the map.
	 * @param dx
	 *            The shift in x direction in tiles.
	 * @param dy
	 *            The shift in y direction in tiles.
	 */
	public void resize(int width, int height, int dx, int dy) {
		for (MapLayer layer : this) {
			if (layer.bounds.equals(this.bounds)) {
				layer.resize(width, height, dx, dy);
			} else {
				layer.setOffset(layer.bounds.x + dx, layer.bounds.y + dy);
			}
		}

		this.bounds.width = width;
		this.bounds.height = height;
	}

	/**
	 * Determines whether the point (x,y) falls within the plane.
	 * 
	 * @param x
	 * @param y
	 * @return <code>true</code> if the point is within the plane, <code>false</code> otherwise
	 */
	public boolean inBounds(int x, int y) {
		return (x >= 0) && (y >= 0) && (x < this.bounds.width) && (y < this.bounds.height);
	}

	@Override
	public Iterator<MapLayer> iterator() {
		return this.layers.iterator();
	}

	/**
	 * Adds a Tileset to this Map. If the set is already attached to this map, <code>addTileset</code> simply returns.
	 * 
	 * @param tileset
	 *            a tileset to add
	 */
	public void addTileset(TileSet tileset) {
		if ((tileset == null) || (this.tileSets.indexOf(tileset) > -1)) {
			return;
		}

		Tile t = tileset.getTile(0);

		if (t != null) {
			int tw = t.getWidth();
			int th = t.getHeight();
			if (tw != this.tileWidth) {
				if (this.tileWidth == 0) {
					this.tileWidth = tw;
					this.tileHeight = th;
				}
			}
		}

		this.tileSets.add(tileset);
	}

	/**
	 * Removes a {@link TileSet} from the map, and removes any tiles in the set from the map layers.
	 * 
	 * @param tileset
	 *            TileSet to remove
	 */
	public void removeTileset(TileSet tileset) {
		// Sanity check
		final int tilesetIndex = this.tileSets.indexOf(tileset);
		if (tilesetIndex == -1) {
			return;
		}

		// Go through the map and remove any instances of the tiles in the set
		for (Tile tile : tileset) {
			for (MapLayer ml : this) {
				if (ml instanceof TileLayer) {
					((TileLayer) ml).removeTile(tile);
				}
			}
		}

		this.tileSets.remove(tileset);
	}

	/**
	 * @return the map properties
	 */
	public Properties getProperties() {
		return this.properties;
	}

	public void setProperties(Properties prop) {
		this.properties = prop;
	}

	public void setFilename(String filename) {
		this.filename = filename;
	}

	/**
	 * Sets a new tile width.
	 * 
	 * @param width
	 *            the new tile width
	 */
	public void setTileWidth(int width) {
		this.tileWidth = width;
	}

	/**
	 * Sets a new tile height.
	 * 
	 * @param height
	 *            the new tile height
	 */
	public void setTileHeight(int height) {
		this.tileHeight = height;
	}

	public void setOrientation(int orientation) {
		this.orientation = orientation;
	}

	public String getFilename() {
		return this.filename;
	}

	/**
	 * Returns a vector with the currently loaded tileSets.
	 * 
	 * @return Vector
	 */
	public Vector<TileSet> getTileSets() {
		return this.tileSets;
	}

	/**
	 * Returns width of map in tiles.
	 * 
	 * @return int
	 */
	public int getWidth() {
		return this.bounds.width;
	}

	/**
	 * Returns height of map in tiles.
	 * 
	 * @return int
	 */
	public int getHeight() {
		return this.bounds.height;
	}

	/**
	 * Returns default tile width for this map.
	 * 
	 * @return the default tile width
	 */
	public int getTileWidth() {
		return this.tileWidth;
	}

	/**
	 * Returns default tile height for this map.
	 * 
	 * @return the default tile height
	 */
	public int getTileHeight() {
		return this.tileHeight;
	}

	/**
	 * Returns wether the given tile coordinates fall within the map boundaries.
	 * 
	 * @param x
	 *            The tile-space x-coordinate
	 * @param y
	 *            The tile-space y-coordinate
	 * @return <code>true</code> if the point is within the map boundaries, <code>false</code> otherwise
	 */
	public boolean contains(int x, int y) {
		return (x >= 0) && (y >= 0) && (x < this.bounds.width) && (y < this.bounds.height);
	}

	/**
	 * Returns the maximum tile height. This is the height of the highest tile in all tileSets or the tile height used
	 * by this map if it's smaller.
	 * 
	 * @return int The maximum tile height
	 */
	public int getTileHeightMax() {
		int maxHeight = this.tileHeight;

		for (TileSet tileset : this.tileSets) {
			int height = tileset.getTileHeight();
			if (height > maxHeight) {
				maxHeight = height;
			}
		}

		return maxHeight;
	}

	/**
	 * Swaps the tile sets at the given indices.
	 */
	public void swapTileSets(int index0, int index1) {
		if (index0 == index1) {
			return;
		}
		TileSet set = this.tileSets.get(index0);
		this.tileSets.set(index0, this.tileSets.get(index1));
		this.tileSets.set(index1, set);
	}

	/**
	 * Returns the orientation of this map. Orientation will be one of {@link Map#ORIENTATION_ISOMETRIC},
	 * {@link Map#ORIENTATION_ORTHOGONAL}, {@link Map#ORIENTATION_HEXAGONAL} and {@link Map#ORIENTATION_SHIFTED}.
	 * 
	 * @return The orientation from the enumerated set
	 */
	public int getOrientation() {
		return this.orientation;
	}

	/**
	 * Returns string describing the map. The form is <code>Map[width x height
	 * x layers][tileWidth x tileHeight]</code>, for example <code>
	 * Map[64x64x2][24x24]</code>.
	 * 
	 * @return string describing map
	 */
	@Override
	public String toString() {
		return "Map[" + this.bounds.width + "x" + this.bounds.height + "x" + this.getLayerCount() + "][" + this.tileWidth + "x" + this.tileHeight + "]";
	}
}
